package com.jf.nep;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.Map;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector3f;
import org.lwjgl.util.vector.Vector4f;

public class Main {
    // Entry point for the application
    public static void main(String[] args) {
        new Main(args.length > 0 && args[0].equalsIgnoreCase("fullscreen"));
    }
    
    // Setup variables
    private final String WINDOW_TITLE = "NeverEndingProject";
    private final int WIDTH = 800;
    private final int HEIGHT = 600;
    // Shader variables
    private int vsId = 0;
    private int fsId = 0;
    private Matrix4f viewMatrix;
    private Matrix4f modelMatrix;
    private Matrix4f projectionMatrix;
    private FloatBuffer matrix44Buffer;
    private Vector3f cameraPos;
    private boolean quit;
    
    private static class MeshAllocation {
        public MeshAllocation(Mesh mesh) {
            this.mesh = mesh;
        }
        
        public int vaoId;
        public int vboId;
        public int vboiId;
        public Vector3f modelPos;
        public Vector3f modelAngle;
        public Vector3f modelScale;
        public int indicesCount;
        public int pId;
        public int projectionMatrixLocation;
        public int viewMatrixLocation;
        public int modelMatrixLocation;
        public Mesh mesh;
        public int normalMatrixLocation;
        
    }
    
    public TextureLoader textureLoader = new TextureLoader();
        
    private Map<Integer, MeshAllocation> meshes = new HashMap<Integer, MeshAllocation>();
    private int nextMeshId = 0;
    private int cubeId;
    private Vector3f cameraAngle;
    private long time;
    private float dt;
    private long lastTime;
    private Matrix4f directionMatrix;
    private int width;
    private int height;
    private boolean useFullScreen;

    
    public Main(boolean useFullScreen) {
        this.useFullScreen = useFullScreen;
        
        // Initialize OpenGL (Display)
        this.setupOpenGL();
        
        this.setupQuad();
        this.setupShaders();
        this.setupMatrices();
        
        quit = false;
        
        while (!Display.isCloseRequested() && !quit) {
            // Do a single loop (logic/render)
            this.loopCycle();
            
            // Force a maximum FPS of about 60
            //Display.sync(60);
            // Let the CPU synchronize with the GPU if GPU is tagging behind
            Display.update();
        }
        
        // Destroy OpenGL (Display)
        this.destroyOpenGL();
    }
       
    public void setupMatrices() {
        projectionMatrix = new Matrix4f();
        viewMatrix = new Matrix4f();
        modelMatrix = new Matrix4f();
        
        float fieldOfView = 65f;
        float aspectRatio = (float)width/(float)height;
        float nearPlane = 0.1f;
        float farPlane = 100f;
        
        float yScale = MathUtil.coTangent(MathUtil.degreesToRadians(fieldOfView / 2f));
        float xScale = yScale / aspectRatio;        
        float frustrumLength = farPlane - nearPlane;
        
        projectionMatrix.m00 = xScale;
        projectionMatrix.m11 = yScale;
        projectionMatrix.m22 = -((farPlane + nearPlane) / frustrumLength);
        projectionMatrix.m23 = -1;
        projectionMatrix.m32 = -((2 * nearPlane * farPlane) / frustrumLength);
        
        matrix44Buffer = BufferUtils.createFloatBuffer(16);
    }

    public void setupOpenGL() {
        
        // Setup an OpenGL context with API version 3.2
        try {
            PixelFormat pixelFormat = new PixelFormat();
            ContextAttribs contextAtrributes = new ContextAttribs(3, 2).withProfileCore(true);
            contextAtrributes.withForwardCompatible(true);
            contextAtrributes.withProfileCore(true);

            if (useFullScreen) {
                Display.setFullscreen(true);
                Display.setVSyncEnabled(true);
            } else {
                Display.setDisplayMode(new DisplayMode(WIDTH,HEIGHT));
            }
            Display.setTitle(WINDOW_TITLE);
            Display.create(pixelFormat, contextAtrributes);
            
            width = Display.getWidth();
            height = Display.getHeight();
            GL11.glViewport(0, 0, width, height);
            
        } catch (LWJGLException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        System.out.println("OpenGL version: " + GL11.glGetString(GL11.GL_VERSION));
               
        // Setup an XNA like background color
        GL11.glClearColor(0.2f,0.2f,0.2f, 0f);
        
        // Map the internal OpenGL coordinate system to the entire screen
        GL11.glViewport(0, 0, width, height);
        
        GL11.glEnable(GL11.GL_DEPTH_TEST);

        Mouse.setGrabbed(true);
    }
    
    public int addMesh(Mesh mesh) {
        nextMeshId++;
        MeshAllocation allocation = new MeshAllocation(mesh);
        meshes.put(nextMeshId, allocation);
        
        // Put each 'Vertex' in one FloatBuffer
        
        FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(mesh.vertices.length *
                TexturedVertex.elementCount);
        for (int i = 0; i < mesh.vertices.length; i++) {
            // Add position, color and texture floats to the buffer
            verticesBuffer.put(mesh.vertices[i].getElements());
        }        
        verticesBuffer.flip();
        
        // OpenGL expects to draw vertices in counter clockwise order by default

        allocation.indicesCount = mesh.triangles.length;
        ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(allocation.indicesCount);
        indicesBuffer.put(mesh.triangles);
        indicesBuffer.flip();
        
        // Create a new Vertex Array Object in memory and select it (bind)
        allocation.vaoId = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(allocation.vaoId);
        
        // Create a new Vertex Buffer Object in memory and select it (bind)
        allocation.vboId = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, allocation.vboId);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesBuffer, GL15.GL_STATIC_DRAW);
        
     // Put the position coordinates in attribute list 0
        GL20.glVertexAttribPointer(0, TexturedVertex.positionElementCount, GL11.GL_FLOAT, 
                false, TexturedVertex.stride, TexturedVertex.positionByteOffset);
        // Put the color components in attribute list 1
        GL20.glVertexAttribPointer(1, TexturedVertex.colorElementCount, GL11.GL_FLOAT, 
                false, TexturedVertex.stride, TexturedVertex.colorByteOffset);
        // Put the texture coordinates in attribute list 2
        GL20.glVertexAttribPointer(2, TexturedVertex.textureElementCount, GL11.GL_FLOAT, 
                false, TexturedVertex.stride, TexturedVertex.textureByteOffset);     
        GL20.glVertexAttribPointer(3, TexturedVertex.normalElementCount, GL11.GL_FLOAT,
                false, TexturedVertex.stride, TexturedVertex.normalByteOffset);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        
        // Deselect (bind to 0) the VAO
        GL30.glBindVertexArray(0);
        
        // Create a new VBO for the indices and select it (bind) - INDICES
        allocation.vboiId = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, allocation.vboiId);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
        
        return nextMeshId;
    }
    
    public void setupQuad() {
        int woodId = textureLoader.loadPNGTexture("crate.png");
        int stoneId = textureLoader.loadPNGTexture("stone.png");
        int brickId = textureLoader.loadPNGTexture("brick.png");

        float floorsize = 8f;
        float floorscale = 2f;
        
        int meshId = addMesh(Mesh.createSurface(floorsize/floorscale, floorsize/floorscale,  stoneId));
        MeshAllocation floor = meshes.get(meshId);
        
        float floorofs = -1.5f;
        float wallscale = 2f;
        float wallheight = floorsize * 2.5f / 4f;
        
        floor.modelPos = new Vector3f(0,0,floorofs);
        floor.modelAngle = new Vector3f(90f, 0.0f, 0.0f);
        floor.modelScale = new Vector3f(2f*floorscale,2f*floorscale,1);
        
        meshId = addMesh(Mesh.createSurface(4f*wallscale, wallscale, brickId));
        MeshAllocation wall = meshes.get(meshId);
        wall.modelPos = new Vector3f(0, wallheight/2, floorofs+floorsize);
        wall.modelAngle = new Vector3f(0,0,0);
        wall.modelScale = new Vector3f(floorsize/(wallscale*2f),wallheight/wallscale,1);                
        
        meshId = addMesh(Mesh.createSurface(4f*wallscale, wallscale, brickId));
        wall = meshes.get(meshId);
        wall.modelPos = new Vector3f(0, wallheight/2, floorofs-floorsize);
        wall.modelAngle = new Vector3f(0,0,0);
        wall.modelScale = new Vector3f(floorsize/(wallscale*2f),wallheight/wallscale,1);                
        
        meshId = addMesh(Mesh.createSurface(4f*wallscale, wallscale, brickId));
        wall = meshes.get(meshId);
        wall.modelPos = new Vector3f(-floorsize, wallheight/2, floorofs);
        wall.modelAngle = new Vector3f(0, -90, 0);
        wall.modelScale = new Vector3f(floorsize/(wallscale*2f),wallheight/wallscale,1);
        
        meshId = addMesh(Mesh.createSurface(4f*wallscale, wallscale, brickId));
        wall = meshes.get(meshId);
        wall.modelPos = new Vector3f(floorsize, wallheight/2, floorofs);
        wall.modelAngle = new Vector3f(0, -90, 0);
        wall.modelScale = new Vector3f(floorsize/(wallscale*2f),wallheight/wallscale,1);
        
        cubeId = addMesh(Mesh.createCube(1.0f, woodId));
        MeshAllocation cube = meshes.get(cubeId);        
        cube.modelPos = new Vector3f(0,0.5f,-1.9f);
        cube.modelAngle = new Vector3f(0.00f,30.00f,0.00f);
        cube.modelScale = new Vector3f(1,1,1);
        

        meshId = addMesh(Mesh.createCube(1.0f, woodId));
        MeshAllocation cube2 = meshes.get(meshId);
        cube2.modelPos = new Vector3f(0.3f, 0.5f, -3.9f);
        cube2.modelAngle = new Vector3f(0.00f, 80.00f, 0.0f);
        cube2.modelScale = new Vector3f(1,1,1);
        
        cameraPos = new Vector3f(0,-0.9f,0);
        cameraAngle = new Vector3f(0,20f,0);
        
    }
        

    private void setupShaders() {
        int errorCheckValue = GL11.glGetError();
        
        // Load the vertex shader
        vsId = this.loadShader("vertex.glsl", GL20.GL_VERTEX_SHADER);
        // Load the fragment shader
        fsId = this.loadShader("fragment.glsl", GL20.GL_FRAGMENT_SHADER);
        
        for (MeshAllocation allocation : meshes.values()) {
            // Create a new shader program that links both shaders
            allocation.pId = GL20.glCreateProgram();
            GL20.glAttachShader(allocation.pId, vsId);
            GL20.glAttachShader(allocation.pId, fsId);
            GL20.glLinkProgram(allocation.pId);

            // Position information will be attribute 0
            GL20.glBindAttribLocation(allocation.pId, 0, "in_Position");
            // Color information will be attribute 1
            GL20.glBindAttribLocation(allocation.pId, 1, "in_Color");
         // Textute information will be attribute 2
            GL20.glBindAttribLocation(allocation.pId, 2, "in_TextureCoord"); 
            // Normal information will be attribute 3
            GL20.glBindAttribLocation(allocation.pId, 3, "in_Normal");

            allocation.projectionMatrixLocation = GL20.glGetUniformLocation(allocation.pId,  "projectionMatrix");
            allocation.viewMatrixLocation = GL20.glGetUniformLocation(allocation.pId, "viewMatrix");
            allocation.modelMatrixLocation = GL20.glGetUniformLocation(allocation.pId, "modelMatrix");
            allocation.normalMatrixLocation = GL20.glGetUniformLocation(allocation.pId, "normalMatrix");

            GL20.glValidateProgram(allocation.pId);

            errorCheckValue = GL11.glGetError();
            if (errorCheckValue != GL11.GL_NO_ERROR) {
                System.out.println("ERROR - Could not create the shaders:" + GLU.gluErrorString(errorCheckValue));
                System.exit(-1);
            }
        }
    }
    public void loopCycle() {
        logicCycle();
        renderCycle();
    }
    
    public void movePos(Vector3f vector, float delta, float useX, float useZ) {
        Vector4f forward = new Vector4f(useX, 0, useZ, 1);            
        Matrix4f.transform(directionMatrix, forward, forward);
        vector.x -= delta * forward.x * dt;
        vector.z += delta * forward.z * dt;
    }
    
    public void logicCycle() {
        float rotationDelta = 60f;
        //float scaleDelta = 0.1f;
        float posDelta = 3.0f;
        
        //Vector3f scaleAddResolution = new Vector3f(scaleDelta, scaleDelta, scaleDelta);
        //Vector3f scaleMinusResolution = new Vector3f(-scaleDelta, -scaleDelta, -scaleDelta);
        
        MeshAllocation cube = meshes.get(cubeId);
        //MeshAllocation floor = meshes.get(floorId);
        
        time = Sys.getTime();
        dt = (time - lastTime) / 1000.0f;
        lastTime = time;
        
        float dx = Mouse.getDX();
        float dy = Mouse.getDY();
        
        cameraAngle.y += dx * 0.1f;
        cameraAngle.x -= dy * 0.1f;
        if (cameraAngle.x > 45f) {
            cameraAngle.x = 45f;
        }
        if (cameraAngle.x < -45f) {
            cameraAngle.x = -45f;
        }
        
        
        
        directionMatrix = new Matrix4f();
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.y), new Vector3f(0,1,0), directionMatrix, directionMatrix);
        
        if (Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) {
            quit = true;
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_A)) {
            movePos(cameraPos, posDelta, -1, 0);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_D)) {
            movePos(cameraPos, posDelta, 1, 0);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_W)) {
            movePos(cameraPos, posDelta, 0, 1);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_S)) {
            movePos(cameraPos, posDelta, 0, -1);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
            movePos(cube.modelPos, posDelta, 0, 1);
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
            movePos(cube.modelPos, posDelta, 0, -1);
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
            movePos(cube.modelPos, posDelta, 1, 0);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
            movePos(cube.modelPos, posDelta, -1, 0);
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_INSERT)) {
            cube.modelAngle.y += rotationDelta * dt;
        } 
        if (Keyboard.isKeyDown(Keyboard.KEY_DELETE)) {
            cube.modelAngle.y -= rotationDelta * dt;
        }
        
        viewMatrix = new Matrix4f();       
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.z), new Vector3f(0,0,1), viewMatrix, viewMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.x), new Vector3f(1,0,0), viewMatrix, viewMatrix);
        Matrix4f.rotate(MathUtil.degreesToRadians(cameraAngle.y), new Vector3f(0,1,0), viewMatrix, viewMatrix);
        Matrix4f.translate(cameraPos, viewMatrix, viewMatrix);
        
        
        // Upload matrices to the uniform variables
        projectionMatrix.store(matrix44Buffer); 
        matrix44Buffer.flip();
        for (MeshAllocation allocations : meshes.values()) {
            GL20.glUseProgram(allocations.pId);        
            GL20.glUniformMatrix4(allocations.projectionMatrixLocation, false, matrix44Buffer);
        }
        viewMatrix.store(matrix44Buffer);
        matrix44Buffer.flip();
        for (MeshAllocation allocations : meshes.values()) {        
            GL20.glUseProgram(allocations.pId);        
            GL20.glUniformMatrix4(allocations.viewMatrixLocation, false, matrix44Buffer);
        }
        
        for (MeshAllocation allocations : meshes.values()) {
            Matrix4f normalMatrix = new Matrix4f();
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.z), new Vector3f(0,0,1), normalMatrix, normalMatrix);
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.y), new Vector3f(0,1,0), normalMatrix, normalMatrix);
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.x), new Vector3f(1,0,0), normalMatrix, normalMatrix);
            
            modelMatrix = new Matrix4f();        
            Matrix4f.translate(allocations.modelPos, modelMatrix, modelMatrix);
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.z), new Vector3f(0,0,1), modelMatrix, modelMatrix);
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.y), new Vector3f(0,1,0), modelMatrix, modelMatrix);
            Matrix4f.rotate(MathUtil.degreesToRadians(allocations.modelAngle.x), new Vector3f(1,0,0), modelMatrix, modelMatrix);
            Matrix4f.scale(allocations.modelScale, modelMatrix, modelMatrix);

            GL20.glUseProgram(allocations.pId);        
            modelMatrix.store(matrix44Buffer);
            matrix44Buffer.flip();
            GL20.glUniformMatrix4(allocations.modelMatrixLocation, false, matrix44Buffer);
            normalMatrix.store(matrix44Buffer);
            matrix44Buffer.flip();
            GL20.glUniformMatrix4(allocations.normalMatrixLocation, false, matrix44Buffer);
        }
        
        GL20.glUseProgram(0);
    }
    
    public void renderCycle() {
        
        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

        for (MeshAllocation allocation : meshes.values()) {
            GL20.glUseProgram(allocation.pId);

            // Bind the texture
            GL13.glActiveTexture(GL13.GL_TEXTURE0);
            GL11.glBindTexture(GL11.GL_TEXTURE_2D, allocation.mesh.textureId);
            
            // Bind to the VAO that has all the information about the vertices
            GL30.glBindVertexArray(allocation.vaoId);
            GL20.glEnableVertexAttribArray(0);
            GL20.glEnableVertexAttribArray(1);
            GL20.glEnableVertexAttribArray(2);
            GL20.glEnableVertexAttribArray(3);

            // Bind to the index VBO that has all the information about the order of the vertices
            GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, allocation.vboiId);

            // Draw the vertices
            GL11.glDrawElements(GL11.GL_TRIANGLES, allocation.indicesCount, GL11.GL_UNSIGNED_BYTE, 0);

            // Put everything back to default (deselect)
            GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
            GL20.glDisableVertexAttribArray(0);
            GL20.glDisableVertexAttribArray(1);
            GL20.glDisableVertexAttribArray(2);
            GL20.glDisableVertexAttribArray(3);
            GL30.glBindVertexArray(0);
            GL20.glUseProgram(0);
        }
    }
    
    public void destroyOpenGL() {
        textureLoader.cleanup();
        
        // Delete the shaders
        GL20.glUseProgram(0);
        for (MeshAllocation allocation : meshes.values()) {
            GL20.glDetachShader(allocation.pId, vsId);
            GL20.glDetachShader(allocation.pId, fsId);
            GL20.glDeleteProgram(allocation.pId);
        }
        
        GL20.glDeleteShader(vsId);
        GL20.glDeleteShader(fsId);
        
        for (MeshAllocation allocation : meshes.values()) {
            // Select the VAO
            GL30.glBindVertexArray(allocation.vaoId);

            // Disable the VBO index from the VAO attributes list
            GL20.glDisableVertexAttribArray(0);
            GL20.glDisableVertexAttribArray(1);
            GL20.glDisableVertexAttribArray(2);
            GL20.glDisableVertexAttribArray(3);

            // Delete the vertex VBO
            GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
            GL15.glDeleteBuffers(allocation.vboId);

            // Delete the index VBO
            GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
            GL15.glDeleteBuffers(allocation.vboiId);

            // Delete the VAO
            GL30.glBindVertexArray(0);
            GL30.glDeleteVertexArrays(allocation.vaoId);
        }
        
        Display.destroy();
    }
    
    public int loadShader(String url, int type) {
        StringBuilder shaderSource = new StringBuilder();
        int shaderID = 0;
        
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(url)));
            String line;
            while ((line = reader.readLine()) != null) {
                shaderSource.append(line).append("\n");
            }
            reader.close();
        } catch (IOException e) {
            System.err.println("Could not read file.");
            e.printStackTrace();
            System.exit(-1);
        }
        
        shaderID = GL20.glCreateShader(type);
        GL20.glShaderSource(shaderID, shaderSource);
        GL20.glCompileShader(shaderID);

        if (GL20.glGetShader(shaderID, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
            String st = GL20.glGetShaderInfoLog(shaderID, 100);
            System.err.println("Could not compile shader: " + url + " : " + st);
            System.exit(-1);
        }
        
        return shaderID;
    }
}
